package com.xyzwps.lib.openapi.element;

import com.xyzwps.lib.bedrock.Args;
import com.xyzwps.lib.json.element.JsonArray;
import com.xyzwps.lib.json.element.JsonObject;
import lombok.Getter;
import lombok.Setter;

import java.util.*;

import static com.xyzwps.lib.dollar.Dollar.*;

/**
 * OpenAPI Specification used JSON Schema.
 *
 * @see <a href="https://json-schema.org/draft/2020-12/json-schema-validation">JSON Schema Validation</a>
 * @see <a href="https://swagger.io/specification">OpenAPI Specification 3.1.0</a>
 */
public sealed interface Schema extends OASElement {

    /**
     * Accepts a visitor.
     *
     * @param visitor accepted visitor
     */
    @Override
    default void accept(OAEVisitor visitor) {
        visitor.visit(this);
    }

    void accept(SchemaVisitor visitor);

    <T> void accept(SchemaVisitor1<T> visitor, T t);

    /**
     * A schema reference.
     *
     * @param $ref reference path
     */
    record RefSchema(String $ref) implements Schema {
        @Override
        public void accept(SchemaVisitor visitor) {
            visitor.visit(this);
        }

        @Override
        public <T> void accept(SchemaVisitor1<T> visitor, T t) {
            visitor.visit(this, t);
        }
    }

    /**
     * A allOf schema.
     */
    @Getter
    final class AllOf implements Schema {
        @Setter
        private String title;
        @Setter
        private String description;
        @Setter
        private Boolean deprecated;

        private final ArrayList<Schema> schemas = new ArrayList<>();

        public AllOf add(Schema schema) {
            schemas.add(Args.notNull(schema, "schema cannot be null"));
            return this;
        }

        @Override
        public void accept(SchemaVisitor visitor) {
            visitor.visit(this);
        }

        @Override
        public <T> void accept(SchemaVisitor1<T> visitor, T t) {
            visitor.visit(this, t);
        }
    }

    @Getter
    final class OneOf implements Schema {
        @Setter
        private String title;
        @Setter
        private String description;
        @Setter
        private Boolean deprecated;

        private final List<Schema> schemas = new ArrayList<>();

        public OneOf add(Schema schema) {
            schemas.add(Args.notNull(schema, "schema cannot be null"));
            return this;
        }

        @Override
        public void accept(SchemaVisitor visitor) {
            visitor.visit(this);
        }

        @Override
        public <T> void accept(SchemaVisitor1<T> visitor, T t) {
            visitor.visit(this, t);
        }
    }

    @Getter
    final class AnyOf implements Schema {
        @Setter
        private String title;
        @Setter
        private String description;
        @Setter
        private Boolean deprecated;

        private final List<Schema> schemas = new ArrayList<>();

        public AnyOf add(Schema schema) {
            schemas.add(Args.notNull(schema, "schema cannot be null"));
            return this;
        }

        @Override
        public void accept(SchemaVisitor visitor) {
            visitor.visit(this);
        }

        @Override
        public <T> void accept(SchemaVisitor1<T> visitor, T t) {
            visitor.visit(this, t);
        }
    }

    @Getter
    final class ObjectSchema implements Schema {
        @Setter
        private String title;
        @Setter
        private String description;
        @Setter
        private Boolean deprecated;

        private final List<String> required = new ArrayList<>();
        @Setter
        private Integer maxProperties;
        @Setter
        private Integer minProperties;

        private final Map<String, Schema> properties = new TreeMap<>();
        @Setter
        private Schema additionalProperties;
        @Setter
        private JsonObject example;
        @Setter
        private Discriminator discriminator;

        public ObjectSchema addRequired(String... names) {
            for (var name : names) {
                if (name != null) {
                    required.add(name);
                }
            }
            return this;
        }

        public ObjectSchema addProperty(String name, Schema schema) {
            properties.put(name, Args.notNull(schema, "schema cannot be null"));
            return this;
        }

        @Override
        public void accept(SchemaVisitor visitor) {
            visitor.visit(this);
        }

        @Override
        public <T> void accept(SchemaVisitor1<T> visitor, T t) {
            visitor.visit(this, t);
        }
    }

    @Getter
    final class ArraySchema implements Schema {
        @Setter
        private String title;
        @Setter
        private String description;
        @Setter
        private Boolean deprecated;

        private final Schema items;
        @Setter
        private JsonArray example;

        @Setter
        private Integer maxItems;
        @Setter
        private Integer minItems;
        @Setter
        private Boolean uniqueItems;
        @Setter
        private Integer maxContains;
        @Setter
        private Integer minContains;

        public ArraySchema(Schema items) {
            this.items = Args.notNull(items, "items cannot be null");
        }

        @Override
        public void accept(SchemaVisitor visitor) {
            visitor.visit(this);
        }

        @Override
        public <T> void accept(SchemaVisitor1<T> visitor, T t) {
            visitor.visit(this, t);
        }
    }

    @Getter
    final class EnumSchema implements Schema {
        @Setter
        private String title;
        @Setter
        private String description;
        @Setter
        private Boolean deprecated;

        @Setter
        private String example;

        private final TreeSet<String> enums = new TreeSet<>();

        public EnumSchema(String... enums) {
            this.enums.addAll(Arrays.asList(enums));
        }

        public EnumSchema addEnum(String enumValue) {
            if ($.isNotBlank(enumValue)) {
                this.enums.add(enumValue);
            }
            return this;
        }

        @Override
        public void accept(SchemaVisitor visitor) {
            visitor.visit(this);
        }

        @Override
        public <T> void accept(SchemaVisitor1<T> visitor, T t) {
            visitor.visit(this, t);
        }
    }

    @Getter
    final class StringSchema implements Schema {
        @Setter
        private String title;
        @Setter
        private String description;
        @Setter
        private Boolean deprecated;

        @Setter
        private String defaultValue;
        private SchemaFormat format;
        @Setter
        private String example;

        @Setter
        private Integer maxLength;
        @Setter
        private Integer minLength;
        @Setter
        private String pattern;

        public StringSchema format(SchemaFormat f) {
            this.format = switch (f) {
                case null -> null;
                case UNKNOWN, INT32, INT64, FLOAT, DOUBLE -> null;
                default -> f;
            };
            return this;
        }

        @Override
        public void accept(SchemaVisitor visitor) {
            visitor.visit(this);
        }

        @Override
        public <T> void accept(SchemaVisitor1<T> visitor, T t) {
            visitor.visit(this, t);
        }
    }

    @Getter
    @Setter
    final class BooleanSchema implements Schema {
        private String title;
        private String description;
        private Boolean deprecated;

        @Override
        public void accept(SchemaVisitor visitor) {
            visitor.visit(this);
        }

        @Override
        public <T> void accept(SchemaVisitor1<T> visitor, T t) {
            visitor.visit(this, t);
        }
    }

    @Getter
    final class NumberSchema implements Schema {
        @Setter
        private String title;
        @Setter
        private String description;
        @Setter
        private Boolean deprecated;

        @Setter
        private Long defaultValue;
        @Setter
        private Long example;

        private SchemaFormat format;

        @Setter
        private Double multipleOf;
        private Double maximum;
        private Double minimum;
        private Double exclusiveMaximum;
        private Double exclusiveMinimum;

        public NumberSchema maximum(Double maximum) {
            this.maximum = maximum;
            this.exclusiveMaximum = null;
            return this;
        }

        public NumberSchema exclusiveMaximum(Double exclusiveMaximum) {
            this.exclusiveMaximum = exclusiveMaximum;
            this.maximum = null;
            return this;
        }

        public NumberSchema minimum(Double minimum) {
            this.minimum = minimum;
            this.exclusiveMinimum = null;
            return this;
        }

        public NumberSchema exclusiveMinimum(Double exclusiveMinimum) {
            this.exclusiveMinimum = exclusiveMinimum;
            this.minimum = null;
            return this;
        }

        public NumberSchema format(SchemaFormat f) {
            this.format = switch (f) {
                case FLOAT, DOUBLE -> f;
                default -> null;
            };
            return this;
        }

        @Override
        public void accept(SchemaVisitor visitor) {
            visitor.visit(this);
        }

        @Override
        public <T> void accept(SchemaVisitor1<T> visitor, T t) {
            visitor.visit(this, t);
        }
    }

    @Getter
    final class IntegerSchema implements Schema {
        @Setter
        private String title;
        @Setter
        private String description;
        @Setter
        private Boolean deprecated;

        @Setter
        private Long defaultValue;
        @Setter
        private Long example;

        private SchemaFormat format;

        @Setter
        private Long multipleOf;
        private Long maximum;
        private Long minimum;
        private Long exclusiveMaximum;
        private Long exclusiveMinimum;

        public IntegerSchema maximum(Long maximum) {
            this.maximum = maximum;
            this.exclusiveMaximum = null;
            return this;
        }

        public IntegerSchema exclusiveMaximum(Long exclusiveMaximum) {
            this.exclusiveMaximum = exclusiveMaximum;
            this.maximum = null;
            return this;
        }

        public IntegerSchema minimum(Long minimum) {
            this.minimum = minimum;
            this.exclusiveMinimum = null;
            return this;
        }

        public IntegerSchema exclusiveMinimum(Long exclusiveMinimum) {
            this.exclusiveMinimum = exclusiveMinimum;
            this.minimum = null;
            return this;
        }

        public IntegerSchema format(SchemaFormat f) {
            this.format = switch (f) {
                case INT32, INT64 -> f;
                default -> null;
            };
            return this;
        }

        @Override
        public void accept(SchemaVisitor visitor) {
            visitor.visit(this);
        }

        @Override
        public <T> void accept(SchemaVisitor1<T> visitor, T t) {
            visitor.visit(this, t);
        }
    }
}
